﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.IO;
using elibrary.Classes;
using elibrary.Models.Managers;

namespace elibrary.Models.Entities
{
    public class Book
    {
        /* Class Fields [Start] ----------------------------------------------------------------- */
		[Key]
		public int id { get; set; }
		public string isbn { get; set; }
		public string title { get; set; }
		public string author { get; set; }
		public BookType type { get; set; }
		public BookQuality quality { get; set; }
		public string publisher { get; set; }
		public string yearPublished { get; set; }
		public string description { get; set; }
		public string image { get; set; }
		public int uploaderId { get; set; }
		public DateTime dateAdded { get; set; }
		public decimal price { get; set; }
		public BookStatus status { get; set; }
		/* Class Fields [End] ------------------------------------------------------------------- */

		/* Computed Fields [Start] -------------------------------------------------------------- */
		public string[] authors
		{
			get { return Utilities.valuesOfCSV(this.author); }
		}

        public string conciseAuthors
        {
            get { return this.getConciseAuthors(this.authors); }
        }

		public string[] images
		{
			get { return this.image.Split(':'); }
		}

		public bool hasImage
		{
			get { return !string.IsNullOrWhiteSpace(this.image); }
		}

		public int imageCount
		{
			get { return this.hasImage ? this.images.Length : 0; }
		}

		[NotMapped]
		public string categories
		{
			get { return this.getCategoryString(); }
			set { this.setCategoryString(value.ToLower()); }
		}

		public string properISBN
		{
			get { return this.getFormattedISBN(this.isbn); }
		}

		public User uploader
		{
			get { return this.getUploader(this.uploaderId); }
		}

		public List<BookCategory> bookCategories
		{
			get { return this.getBookCategories(this.id); }
		}

        public string qualityProper
        {
            get { return Book.describeBookQuality(this.quality); }
        }

        public string typeProper
        {
            get { return Book.describeBookType(this.type); }
        }

        public string statusProper
        {
            get { return Book.describeStatus(this.status); }
        }

        public bool isAvailable
        {
            get { return this.status == BookStatus.Available; }
        }

        public bool isBeingReviewed
        {
            get { return this.status == BookStatus.InReview; }
        }

        public bool isSold
        {
            get { return this.status == BookStatus.Sold; }
        }

        public bool isDeleted
        {
            get { return this.status == BookStatus.Deleted; }
        }

        public bool isSecondhand
        {
            get { return this.type == BookType.Secondhand; }
        }

        public Dictionary<string,object> clientRichObject
        {
            get { return this.getClientRichObject(); }
        }

        public Dictionary<string,object> marketplaceObject
        {
            get { return this.getMarketplaceObject(); }
        }
		/* Computed Fields [End] ---------------------------------------------------------------- */

		/* Class Enums [Start] ------------------------------------------------------------------ */
		public enum BookStatus { Available = 0, InReview, Deleted, Sold }
		public enum BookQuality { NotAcceptable = 1, RequireRepairs, Moderate, QuiteGood, Good,
			Excellent }
		public enum BookType { Firsthand = 0, Secondhand }
		/* Class Enums [End] -------------------------------------------------------------------- */

        public static string describeBookQuality(BookQuality q)
        {
            switch (q)
            {
                case BookQuality.NotAcceptable:
                    return "Not Acceptable";
                case BookQuality.RequireRepairs:
                    return "Require Repairs";
                case BookQuality.Moderate:
                    return "Moderate";
                case BookQuality.QuiteGood:
                    return "Quite Good";
                case BookQuality.Good:
                    return "Good";
                case BookQuality.Excellent:
                    return "Excellent";
                default:
                    return "-";
            }
        }

        public static string describeBookType(BookType t)
        {
            switch (t)
            {
                case BookType.Firsthand:
                    return "Firsthand";
                case BookType.Secondhand:
                    return "Secondhand";
                default:
                    return "-";
            }
        }

        public static string describeStatus(BookStatus s)
        {
            switch (s)
            {
                case BookStatus.Available:
                    return "Available";
                case BookStatus.InReview:
                    return "In Review";
                case BookStatus.Deleted:
                    return "Deleted";
                case BookStatus.Sold:
                    return "Sold";
                default:
                    return "-";
            }
        }

        public bool makeAvailable()
        {
            if (this.hasImage && this.imageCount > 1 && this.status != BookStatus.Deleted)
            {
                if (this.isBeingReviewed)
                    Notice.notifyBookApproved(this.uploaderId,this.id);

                this.status = BookStatus.Available;

                return true;
            }

            return false;
        }

        public bool fileForReview()
        {
            if (this.status == BookStatus.Sold || this.status == BookStatus.Deleted)
                return false;

            this.status = BookStatus.InReview;
            if (!this.hasImage)
                Notice.notifyBookUploadImg(this.uploaderId,this.id);

            return true;
        }

        public bool deleteThis()
        {
            if (this.status == BookStatus.Deleted || this.status == BookStatus.Sold)
                return false;

            this.status = BookStatus.Deleted;

            return true;
        }

        public bool sellThis()
        {
            if (this.status == BookStatus.Available && this.type == BookType.Secondhand)
            {
                this.status = BookStatus.Sold;
                Notice.notifyBookSold(this.uploaderId,this.id);
            }

            return true;
        }

        public bool remitThis()
        {
            if (this.status == BookStatus.Sold && this.type == BookType.Secondhand)
            {
                this.status = BookStatus.Available;
                Notice.notifyBookRemitted(this.uploaderId,this.id);
            }

            return true;
        }

        public decimal calcUserProfit()
        {
            if (this.type != BookType.Secondhand)
                return 0.0m;

            return this.price * AppGlobals.glbs<decimal>("profit_rate",0.87m);
        }

        private string getCategoryString()
        {
            return string.Join(",",this.buildCategoriesArray());
        }

        private string[] buildCategoriesArray()
        {
        	List<string> cs = new List<string>();

        	foreach(var bc in this.bookCategories)
        		cs.Add(bc.category.name);

        	return cs.ToArray();
        }

        // this method will cause side effects to the BookCategory and Category
        // collection hence changes are committed at end of the method call.
        private void setCategoryString(string cs)
        {
        	BookCategoryManager mgr = new BookCategoryManager(AppGlobals.dataContext);
        	string[] ncs = Utilities.redundantFreeCSV(cs);
        	string[] ocs = this.buildCategoriesArray();

        	// delete categories marked for deletion first,
        	// i.e. in ocs but not in ncs
        	var ws = ocs.Except(ncs);
        	foreach (var wk in ws)
        		mgr.deleteByBook(this.id,wk);

        	// add new categories, i.e. in ncs but not in ocs
        	ws = ncs.Except(ocs);
        	foreach (var wk in ws)
        		mgr.addForBook(this.id,wk);

            AppGlobals.dataContext.SaveChanges();
        }

        private string getFormattedISBN(string i)
        {
            int len = i.Length;

            return len == 13 ? i.Substring(0, 3) + "-" + i.Substring(3, 1) + "-" 
                + i.Substring(4, 3) + "-" + i.Substring(7, 5) + "-" + i.Substring(12, 1)
                : i.Substring(0, 1) + "-" + i.Substring(1, 3) + "-" + i.Substring(4, 5) 
                + "-" + i.Substring(9, 1);
        }

        public string getFormattedAuthors(string athrs, bool full=true)
        {
        	string[] tokens = Utilities.valuesOfCSV(athrs);
        	int nauthr = tokens.Length;
        	bool multi = nauthr > 1;

        	string formatted = tokens[0];
        	if (!full || !multi)
        		return multi ? formatted + " et al." : formatted;

        	for (int i = 1; i < nauthr - 1; ++i)
        		formatted += ", " + tokens[i];

        	return formatted + " and " + tokens[nauthr - 1];
        }

        private User getUploader(int uid)
        {
            return (new UserManager(AppGlobals.dataContext)).get(uid);
        }

        private List<BookCategory> getBookCategories(int uid)
        {
            return (new BookCategoryManager(AppGlobals.dataContext)).getAllByBook(uid);
        }

        // TODO: use callback method for saving of image instead of using
        // any of the http classes in the business entities.
        public bool addImage(string repo, HttpPostedFileBase img)
        {
        	if (string.IsNullOrWhiteSpace(repo) || img == null || img.ContentLength < 1)
        		return false;

    		string ext = Path.GetExtension(Path.GetFileName(img.FileName));
    		//string repo = current.Server.MapPath("~/Uploads/Book/Images");
    		string fname = generateLocalImageName(repo,ext);

    		try 
    		{
                ensureUploadDir(repo);
    			img.SaveAs(repo + "/" + fname);
    		}
    		catch (Exception ex)
    		{
                // TODO: add an exception listener and logger
    			return false;
    		}

    		appendToImageString(fname);

    		return true;
        }

        public string getImageForEditBook()
        {
        	if (!this.hasImage)
        		return "<option value=\"NA\">Please Upload Image</option>";

        	string html = "";
        	int ind = 0;
        	foreach(var img in this.images)
        		html += "<option value=\"" + ind++ + "\">" + img + "</option>\n";

        	return html;
        }

        // TODO: Create a new class for carrying info about image to be
        // displayed on the View table.
        public object[] getImageForTable()
        {
        	if (!this.hasImage)
        		return null;

            List<object> dumped = new List<object>();
            int ind = 0;
            foreach(var img in this.images)
                dumped.Add(new { id = ind, image = img, bookId = id});

        	return dumped.ToArray();
        }

        public bool deleteImage(string repo, string fname)
        {
        	if (string.IsNullOrWhiteSpace(repo))
        		return false;

        	string fullpath = repo + "/" + fname;
        	try
        	{
	        	if (File.Exists(fullpath))
	        		File.Delete(fullpath);
        	}
        	catch (Exception ex)
        	{
        		return false;
        	}

        	return deleteFromImageString(fname);
        }

        private bool appendToImageString(string fname)
        {
        	string ws = this.image;
        	if (string.IsNullOrWhiteSpace(ws) || ws[ws.Length - 1] != ':')
        	{
        		this.image = fname;
        		return true;
        	}

        	ws += (ws[ws.Length - 1] == ':' ? "" : ":") + fname;
        	this.image = ws;

        	return true;
        }

        private bool insertToImageString(string fname, int pos)
        {
        	List<string> ws = new List<string>(this.images);

        	if (pos < 0 || pos > ws.Count)
        		return false;
        	if (pos == ws.Count)    	
    			return appendToImageString(fname);

    		ws.Insert(pos,fname);

    		this.image = string.Join(":",ws.ToArray());

    		return true;
        }

        private bool deleteFromImageString(int pos)
        {
        	List<string> ws = new List<string>(this.images);

        	if (pos < 0 || pos >= ws.Count)
        		return false;

        	ws.RemoveAt(pos);

        	this.image = string.Join(":",ws.ToArray());

        	return true;
        }

        private bool deleteFromImageString(string fname)
        {
        	List<string> ws = new List<string>(this.images);

        	if (!ws.Remove(fname))
        		return false;

        	this.image = string.Join(":",ws.ToArray());

        	return true;
        }

        private string generateLocalImageName(string localDir, string ext, int len=15)
        {
        	string fname = "" + this.id + "-" + Utilities.randomizeString(len,true,true) + ext;
        	string fullpath = localDir + "/" + fname;

        	while (File.Exists(fullpath))
        	{
        		fname = "" + this.id + "-" + Utilities.randomizeString(len,true,true) + ext;
        		fullpath = localDir + "/" + fname;
        	}

        	return fname;
        }

        private void ensureUploadDir(string localDir)
        {
        	if (!Directory.Exists(localDir))
                Directory.CreateDirectory(localDir);
        }

        private string getConciseAuthors(string[] authors)
        {
            return authors.Length > 1 ? authors[0] + " et al." : authors[0];
        }

        private Dictionary<string,object> toClientRichObject()
        {
            Dictionary<string,object> obj = Utilities.getClientRichObject(new string[]
                {"id","isbn","title","price"}, this);

            obj["status"] = (int)this.status;
            obj["type"] = (int)this.type;
            obj["quality"] = (int)this.quality;

            List<Dictionary<string,object> ctgs = new List<Dictionary<string,object>>();
            List<BookCategory> bcs = this.bookCategories;
            foreach(var bc in bcs)
                ctgs.Add(bc.marketplaceObject);
            
            obj["categories"] = ctgs;
            obj["categoryStr"] = this.categories;

            return obj;
        }

        private Dictionary<string,object> getMarketplaceObject()
        {
            Dictionary<string,object> obj = this.toClientRichObject();

            Dictionary<string,object> bType = new Dictionary<string,object>();
            bType["id"] = (int)this.type;
            bType["key"] = this.type != BookType.Secondhand ? "new-book" : "Secondhand";
            bType["desc"] = this.type != BookType.Secondhand ? "Brand New" : "Secondhand Book";
            obj["type"] = bType;

            obj["description"] = this.description;
            obj["author"] = this.conciseAuthors;
            obj["authors"] = this.authors;
            obj["isbn"] = this.properISBN;

            Dictionary<string,object> bQuality = new Dictionary<string,object>();
            bQuality["key"] = (int)this.quality;
            bQuality["desc"] = this.describeBookQuality(this.quality);
            obj["quality"] = bQuality;

            obj["publisher"] = this.publisher;
            obj["yearPublished"] = this.yearPublished;
            obj["images"] = this.images;
            obj["uploader"] = this.uploader.isAdmin && this.type != BookType.Secondhand ?
                "e-Library" : this.uploader.loginId;
        }
    }
}